| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { IS_PLATFORM, useFeatureFlags, useFlag, useParams } from 'common'
- import dayjs, { Dayjs } from 'dayjs'
- import maxBy from 'lodash/maxBy'
- import meanBy from 'lodash/meanBy'
- import sumBy from 'lodash/sumBy'
- import { useRouter } from 'next/router'
- import { useMemo, useState } from 'react'
- import { Alert, AlertDescription, AlertTitle, Button, LogoLoader, WarningIcon } from 'ui'
- import { PageContainer } from 'ui-patterns/PageContainer'
- import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
- import { EdgeFunctionOverview } from '@/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview'
- import { EdgeFunctionRecentInvocations } from '@/components/interfaces/Functions/EdgeFunctionRecentInvocations'
- import ReportWidget from '@/components/interfaces/Reports/ReportWidget'
- import DefaultLayout from '@/components/layouts/DefaultLayout'
- import EdgeFunctionDetailsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
- import AreaChart from '@/components/ui/Charts/AreaChart'
- import StackedBarChart from '@/components/ui/Charts/StackedBarChart'
- import NoPermission from '@/components/ui/NoPermission'
- import {
- FunctionsCombinedStatsVariables,
- useFunctionsCombinedStatsQuery,
- } from '@/data/analytics/functions-combined-stats-query'
- import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
- import { useFillTimeseriesSorted } from '@/hooks/analytics/useFillTimeseriesSorted'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import type { ChartIntervals, NextPageWithLayout } from '@/types'
- const CHART_INTERVALS: ChartIntervals[] = [
- {
- key: '15min',
- label: '15 min',
- startValue: 15,
- startUnit: 'minute',
- format: 'MMM D, h:mm:ssa',
- },
- {
- key: '1hr',
- label: '1 hour',
- startValue: 1,
- startUnit: 'hour',
- format: 'MMM D, h:mma',
- },
- {
- key: '3hr',
- label: '3 hours',
- startValue: 3,
- startUnit: 'hour',
- format: 'MMM D, h:mma',
- },
- {
- key: '1day',
- label: '1 day',
- startValue: 1,
- startUnit: 'hour',
- format: 'MMM D, h:mma',
- },
- ]
- const LegacyEdgeFunctionOverview = () => {
- const router = useRouter()
- const { ref: projectRef, functionSlug } = useParams()
- const [interval, setInterval] = useState<string>('15min')
- const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
- const { data: selectedFunction } = useEdgeFunctionQuery({
- projectRef,
- slug: functionSlug,
- })
- const id = selectedFunction?.id
- const combinedStatsResults = useFunctionsCombinedStatsQuery({
- projectRef,
- functionId: id,
- interval: selectedInterval.key as FunctionsCombinedStatsVariables['interval'],
- })
- const combinedStatsData = useMemo(() => {
- const result = combinedStatsResults.data?.result as
- | Record<string, string | number>[]
- | undefined
- return result || []
- }, [combinedStatsResults.data])
- const [startDate, endDate]: [Dayjs, Dayjs] = useMemo(() => {
- const start = dayjs()
- .subtract(selectedInterval.startValue, selectedInterval.startUnit as dayjs.ManipulateType)
- .startOf(selectedInterval.startUnit as dayjs.ManipulateType)
- const end = dayjs().startOf(selectedInterval.startUnit as dayjs.ManipulateType)
- return [start, end]
- }, [selectedInterval])
- const {
- data: combinedStatsChartData,
- error: combinedStatsError,
- isError: isErrorCombinedStats,
- } = useFillTimeseriesSorted({
- data: combinedStatsData,
- timestampKey: 'timestamp',
- valueKey: [
- 'requests_count',
- 'log_count',
- 'log_info_count',
- 'log_warn_count',
- 'log_error_count',
- 'success_count',
- 'redirect_count',
- 'client_err_count',
- 'server_err_count',
- 'avg_cpu_time_used',
- 'avg_memory_used',
- 'avg_execution_time',
- 'max_execution_time',
- 'avg_heap_memory_used',
- 'avg_external_memory_used',
- 'max_cpu_time_used',
- ],
- defaultValue: 0,
- startDate: startDate.toISOString(),
- endDate: endDate.toISOString(),
- })
- const { isLoading: permissionsLoading, can: canReadFunction } = useAsyncCheckPermissions(
- PermissionAction.FUNCTIONS_READ,
- functionSlug as string
- )
- if (!canReadFunction && !permissionsLoading) {
- return <NoPermission isFullPage resourceText="access this edge function" />
- }
- return (
- <PageContainer size="full">
- <PageSection>
- <PageSectionContent>
- {IS_PLATFORM && id && (
- <div className="mb-8">
- <EdgeFunctionRecentInvocations
- functionId={id}
- functionSlug={functionSlug as string}
- />
- </div>
- )}
- <div className="flex flex-row items-center gap-2 mb-4">
- <div className="flex items-center">
- {CHART_INTERVALS.map((item, i) => {
- const classes = []
- if (i === 0) {
- classes.push('rounded-tr-none rounded-br-none')
- } else if (i === CHART_INTERVALS.length - 1) {
- classes.push('rounded-tl-none rounded-bl-none')
- } else {
- classes.push('rounded-none')
- }
- return (
- <Button
- key={`function-filter-${i}`}
- type={interval === item.key ? 'secondary' : 'default'}
- onClick={() => setInterval(item.key)}
- className={classes.join(' ')}
- >
- {item.label}
- </Button>
- )
- })}
- </div>
- <span className="text-xs text-foreground-light">
- Statistics for past {selectedInterval.label}
- </span>
- </div>
- <div>
- <div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-2 lg:gap-8">
- <ReportWidget
- title="Execution time"
- tooltip="Average execution time of function invocations"
- data={combinedStatsChartData}
- isLoading={combinedStatsResults.isLoading}
- renderer={(props) => {
- return isErrorCombinedStats ? (
- <Alert variant="warning">
- <WarningIcon />
- <AlertTitle>Failed to reterieve execution time</AlertTitle>
- <AlertDescription>
- {combinedStatsError?.message ?? 'Unknown error'}
- </AlertDescription>
- </Alert>
- ) : (
- <div className="space-y-8">
- <AreaChart
- title="Average execution time"
- className="w-full"
- xAxisKey="timestamp"
- customDateFormat={selectedInterval.format}
- yAxisKey="avg_execution_time"
- data={props.data}
- format="ms"
- highlightedValue={meanBy(props.data, 'avg_execution_time')}
- />
- <AreaChart
- title="Max execution time"
- className="w-full"
- xAxisKey="timestamp"
- customDateFormat={selectedInterval.format}
- yAxisKey="max_execution_time"
- data={props.data}
- format="ms"
- highlightedValue={
- maxBy(props.data, 'max_execution_time')?.max_execution_time
- }
- />
- </div>
- )
- }}
- />
- <ReportWidget
- title="Invocations"
- tooltip="Requests made to a function are considered invocations, and each invocation may have worker logs."
- data={combinedStatsChartData}
- isLoading={combinedStatsResults.isLoading}
- renderer={(props) => {
- if (isErrorCombinedStats) {
- return (
- <Alert variant="warning">
- <WarningIcon />
- <AlertTitle>Failed to reterieve invocations</AlertTitle>
- <AlertDescription>
- {combinedStatsError?.message ?? 'Unknown error'}
- </AlertDescription>
- </Alert>
- )
- } else {
- const requestData = props.data
- .map((d: any) => [
- {
- status: '2xx',
- count: d.success_count,
- timestamp: d.timestamp,
- },
- {
- status: '3xx',
- count: d.redirect_count,
- timestamp: d.timestamp,
- },
- {
- status: '4xx',
- count: d.client_err_count,
- timestamp: d.timestamp,
- },
- {
- status: '5xx',
- count: d.server_err_count,
- timestamp: d.timestamp,
- },
- ])
- .flat()
- const logsData = props.data
- .map((d: any) => [
- {
- status: 'error',
- count: d.log_error_count,
- timestamp: d.timestamp,
- },
- {
- status: 'info',
- count: d.log_info_count,
- timestamp: d.timestamp,
- },
- {
- status: 'warn',
- count: d.log_warn_count,
- timestamp: d.timestamp,
- },
- ])
- .flat()
- return (
- <div className="space-y-8">
- <StackedBarChart
- title="Invocation Requests"
- className="w-full"
- xAxisKey="timestamp"
- yAxisKey="count"
- stackKey="status"
- data={requestData}
- highlightedValue={sumBy(requestData, 'count')}
- customDateFormat={selectedInterval.format}
- stackColors={['brand', 'slate', 'yellow', 'red']}
- onBarClick={() => {
- router.push(
- `/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}`
- )
- }}
- />
- <StackedBarChart
- title="Worker Logs"
- className="w-full"
- xAxisKey="timestamp"
- yAxisKey="count"
- stackKey="status"
- data={logsData}
- highlightedValue={sumBy(logsData, 'count')}
- customDateFormat={selectedInterval.format}
- stackColors={['red', 'brand', 'yellow']}
- onBarClick={() => {
- router.push(
- `/project/${projectRef}/functions/${functionSlug}/logs?its=${startDate.toISOString()}`
- )
- }}
- />
- </div>
- )
- }
- }}
- />
- <ReportWidget
- title="CPU time"
- tooltip="Average CPU time usage for the function"
- data={combinedStatsChartData}
- isLoading={combinedStatsResults.isLoading}
- renderer={(props) => {
- return isErrorCombinedStats ? (
- <Alert variant="warning">
- <WarningIcon />
- <AlertTitle>Failed to retrieve CPU time</AlertTitle>
- <AlertDescription>
- {combinedStatsError?.message ?? 'Unknown error'}
- </AlertDescription>
- </Alert>
- ) : (
- <div className="space-y-8">
- <AreaChart
- title="Average CPU Time"
- className="w-full"
- xAxisKey="timestamp"
- customDateFormat={selectedInterval.format}
- yAxisKey="avg_cpu_time_used"
- data={props.data}
- format="ms"
- highlightedValue={meanBy(props.data, 'avg_cpu_time_used')}
- />
- <AreaChart
- title="Max CPU Time"
- className="w-full"
- xAxisKey="timestamp"
- customDateFormat={selectedInterval.format}
- yAxisKey="max_cpu_time_used"
- data={props.data}
- format="ms"
- highlightedValue={maxBy(props.data, 'max_cpu_time_used')?.max_cpu_time_used}
- />
- </div>
- )
- }}
- />
- <ReportWidget
- title="Memory"
- tooltip="Average memory usage for the function"
- data={combinedStatsChartData}
- isLoading={combinedStatsResults.isLoading}
- renderer={(props) => {
- if (isErrorCombinedStats) {
- return (
- <Alert variant="warning">
- <WarningIcon />
- <AlertTitle>Failed to retrieve memory usage</AlertTitle>
- <AlertDescription>
- {combinedStatsError?.message ?? 'Unknown error'}
- </AlertDescription>
- </Alert>
- )
- }
- const memoryData = props.data
- .map((d: any) => [
- {
- type: 'heap',
- count: d.avg_heap_memory_used,
- timestamp: d.timestamp,
- },
- {
- type: 'external',
- count: d.avg_external_memory_used,
- timestamp: d.timestamp,
- },
- ])
- .flat()
- return (
- <div className="space-y-8">
- <AreaChart
- title="Average Memory Usage"
- className="w-full"
- xAxisKey="timestamp"
- customDateFormat={selectedInterval.format}
- yAxisKey="avg_memory_used"
- data={props.data}
- format="MB"
- highlightedValue={meanBy(props.data, 'avg_memory_used')}
- />
- <StackedBarChart
- title="Average Memory Usage by Type"
- className="w-full"
- xAxisKey="timestamp"
- yAxisKey="count"
- stackKey="type"
- format="MB"
- data={memoryData}
- highlightedValue={sumBy(memoryData, 'count')}
- customDateFormat={selectedInterval.format}
- stackColors={['blue', 'brand']}
- />
- </div>
- )
- }}
- />
- </div>
- </div>
- </PageSectionContent>
- </PageSection>
- </PageContainer>
- )
- }
- const PageLayout: NextPageWithLayout = () => {
- const { hasLoaded: flagsLoaded } = useFeatureFlags()
- const showNewOverview = useFlag('edgeFunctionsOverview') === true
- if (IS_PLATFORM && !flagsLoaded) {
- return <LogoLoader />
- }
- if (showNewOverview) {
- return <EdgeFunctionOverview />
- }
- return <LegacyEdgeFunctionOverview />
- }
- PageLayout.getLayout = (page) => (
- <DefaultLayout>
- <EdgeFunctionDetailsLayout title="Overview">{page}</EdgeFunctionDetailsLayout>
- </DefaultLayout>
- )
- export default PageLayout
|